/*
 * sysclk.h
 *
 *  Created on: Sep 18, 2013
 *      Author: Ken Arok
 */

#ifndef SYSCLK_H_
#define SYSCLK_H_

#include <compiler.h>
#include <parts.h>

/* Include clock configuration for the project. */
#include <config_clock.h>

#ifdef __cplusplus
extern "C" {
#endif
#define ASM __asm__

/* CONFIG_SYSCLK_PSDIV  to use default if not defined*/
#ifndef CONFIG_SYSCLK_PSDIV
# define CONFIG_SYSCLK_PSDIV    SYSCLK_PSDIV_8
#endif

/* ! \name Prescaler Setting (relative to CLKsys) */
/* @{ */
#define SYSCLK_PSDIV_1      0   /* !< Do not prescale */
#define SYSCLK_PSDIV_2      1   /* !< Prescale CLKper4 by 2 */
#define SYSCLK_PSDIV_4      2   /* !< Prescale CLKper4 by 4 */
#define SYSCLK_PSDIV_8      3   /* !< Prescale CLKper4 by 8 */
#define SYSCLK_PSDIV_16     4   /* !< Prescale CLKper4 by 16 */
#define SYSCLK_PSDIV_32     5   /* !< Prescale CLKper4 by 32 */
#define SYSCLK_PSDIV_64     6   /* !< Prescale CLKper4 by 64 */
#define SYSCLK_PSDIV_128    7   /* !< Prescale CLKper4 by 128 */
#define SYSCLK_PSDIV_256    8   /* !< Prescale CLKper4 by 256 */
/* @} */


static inline uint32_t sysclk_get_main_hz(void)
{
	switch (SYSCLK_SOURCE) {
		case SYSCLK_SRC_RC8MHZ:
			return 8000000UL;
		case SYSCLK_SRC_RC4MHZ:
			return 4000000UL;
		case SYSCLK_SRC_RC2MHZ:
			return 2000000UL;
		case SYSCLK_SRC_RC1MHZ:
			return 1000000UL;
		case SYSCLK_SRC_XOC12MHZ:
			return 12000000UL;
		case SYSCLK_SRC_OC14MHZ:
			return 14745600UL;
	#ifdef BOARD_EXTERNAL_CLK
		case SYSCLK_SRC_EXTERNAL:
			return BOARD_EXTERNAL_CLK;
	#endif
		default:
			return 1000000UL;
	}
}

/**
 * \brief Return the current rate in Hz of source clock in Hz.
 *
 * This clock always runs at the same rate as the CPU clock unless the divider
 * is set.
 *
 * \return Frequency of the system clock, in Hz.
 */
static inline uint32_t sysclk_get_source_clock_hz(void)
{
	switch (CONFIG_SYSCLK_PSDIV) {
		case SYSCLK_PSDIV_1: /* Fall through */
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 2;
			}
			else {
				return sysclk_get_main_hz();
			}

		case SYSCLK_PSDIV_2:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 4;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 2;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_4:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 8;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 4;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_8:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 16;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 8;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_16:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 32;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 16;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_32:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 64;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 32;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_64:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 128;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 64;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_128:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 256;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 128;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		case SYSCLK_PSDIV_256:
			if (SYSCLK_SOURCE == SYSCLK_SRC_RC16MHZ ||
				SYSCLK_SOURCE == SYSCLK_SRC_RC128KHZ) {
				return sysclk_get_main_hz() / 512;
			}
			else {
			#if CONFIG_SYSCLK_ENABLE_DIV
				return sysclk_get_main_hz() / 256;
			#else
				return sysclk_get_main_hz();
			#endif
			}

		default:
			/*Invalid case*/
			return 0;
	}
}

/**
 * \brief Return the current rate in Hz of the CPU clock.
 *
 * \return Frequency of the CPU clock, in Hz.
 */
static inline uint32_t sysclk_get_cpu_hz(void)
{
	return sysclk_get_source_clock_hz();
}

/* ! \name Enabling and disabling synchronous clocks */
/* @{ */

/**
 * \brief Set system clock prescaler configuration
 *
 * This function will change the system clock prescaler configuration to
 * match the parameters.
 *
 * \note The parameters to this function are device-specific.
 *
 * \param psbcdiv The prescaler  settings (one of the \c SYSCLK_PSCDIV_*
 * definitions). These determine the clkIO, clkADC and clkCPU frequencies.
 * Note: Prescaler setting is not working with the brain dead un optimised code
 * e.g. avr-gcc -00
 */

static inline void sysclk_set_prescalers(uint8_t psdiv)
{
#if MEGA_XX_UN1
#if CONFIG_SYSCLK_ENABLE_DIV
	irqflags_t flags = cpu_irq_save();

	ASM(
			"push r21                    \n\t"
			"ldi  r21, 0x00              \n\t" /* Clear XDIVEN */
			"sts  0x003C, r21            \n\t"
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_1)
			"ldi  r21, 0x00              \n\t" /* divided by (129 - 128) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_2)
			"ldi  r21, 0x7F              \n\t" /* divided by (129 - 127) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_4)
			"ldi  r21, 0x7D              \n\t" /* divided by (129 - 125) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_8)
			"ldi  r21, 0x79              \n\t" /* divided by (129 - 121) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_16)
			"ldi  r21, 0x71              \n\t" /* divided by (129 - 116) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_32)
			"ldi  r21, 0x61              \n\t" /* divided by (129 - 97) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_64)
			"ldi  r21, 0x41              \n\t" /* divided by (129 - 65) */
#endif
#if (CONFIG_SYSCLK_PSDIV == SYSCLK_PSDIV_128)
			"ldi  r21, 0x01              \n\t" /* divided by (129 - 1) */
#endif
			"sts  0x003C, r21            \n\t" /* Write XDIV6:XDIV0 */
			"ori  r21, 0x80              \n\t" /* Set XDIVEN */
			"sts  0x003C, r21            \n\t" /* Enable Clock Divider.*/
			"pop r21                     \n\t"
			);

	cpu_irq_restore(flags);
#endif /* CONFIG_SYSCLK_ENABLE_DIV */
#endif /* MEGA_XX_UN1 */
}


/* ! \name System Clock Initialization */
/* @{ */

extern void sysclk_init(void);

/* ! @} */

#ifdef __cplusplus
}
#endif

#endif /* SYSCLK_H_ */
